Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.

...powered by www.netzwerkartist.de...

 << zurück
Visual C# 2005 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual C# 2005

Visual C# 2005
1.320 S., mit 2 CDs, 59,90 Euro
Galileo Computing
ISBN 3-89842-586-X
gp Kapitel 7 Weitere Möglichkeiten von C#
  gp 7.1 Operatorüberladung
    gp 7.1.1 Die Syntax der Operatorüberladung
    gp 7.1.2 Beispiel einer Operatorüberladung
    gp 7.1.3 Überladungsbeispiele
    gp 7.1.4 Benutzerdefinierte Konvertierungen – implizit und explizit
  gp 7.2 Indexer
    gp 7.2.1 Überladen von Indexern
    gp 7.2.2 Parameterbehaftete Eigenschaften
  gp 7.3 Collections (Auflistungen)
    gp 7.3.1 Die elementaren Schnittstellen der Auflistungsklassen
    gp 7.3.2 Die Klasse »ArrayList«
    gp 7.3.3 Das Sortieren der Elemente einer »ArrayList«
    gp 7.3.4 Die Schnittstelle »IDictionary«
    gp 7.3.5 Die Klasse »Hashtable«
    gp 7.3.6 Die Klassen »Queue« und »Stack«
    gp 7.3.7 Objektauflistungen im Überblick
    gp 7.3.8 Benutzerdefinierte Auflistungen
  gp 7.4 Generics – Generische Datentypen
    gp 7.4.1 Die Typproblematik am Beispiel der Klasse »Stack«
    gp 7.4.2 Die Lösung mit einer generischen Klasse
    gp 7.4.3 Typparameter mit Constraints einschränken
    gp 7.4.4 Generische Methoden
    gp 7.4.5 Generics und Vererbung
    gp 7.4.6 Konvertierung von Generics
    gp 7.4.7 Generische Delegate
    gp 7.4.8 Generische Klassen in der .NET-Klassenbibliothek
    gp 7.4.9 Eigene Auflistungen mit »yield« durchlaufen
    gp 7.4.10 Daten durch »null« beschreiben
  gp 7.5 Fortgeschrittene Delegat-Techniken
    gp 7.5.1 Multicast-Delegate
  gp 7.6 Attribute
    gp 7.6.1 Das »Flags«-Attribut
    gp 7.6.2 Anmerkungen zu den Attributen
    gp 7.6.3 Benutzerdefinierte Attribute
  gp 7.7 Unsicherer Programmcode – Zeigertechnik in C#
    gp 7.7.1 Das Schlüsselwort »unsafe«
    gp 7.7.2 Die Deklaration von Zeigern
    gp 7.7.3 Die »fixed«-Anweisung
    gp 7.7.4 Zeigerarithmetik
    gp 7.7.5 Der Operator »->«


Galileo Computing

7.4 Generics – Generische Datentypen  downtop

Eine der vielleicht wichtigsten Ergänzungen der Sprache C# in der Version 2.0 sind die Generics. Generics ermöglichen die Verwendung von zur Entwicklungszeit unbekannten Datentypen innerhalb von Klassen, Strukturen, Schnittstellen und Methoden. Sie eignen sich insbesondere für Anwendungsfälle, in denen Collections eine Rolle spielen.

Collections sind im Vergleich zu den Arrays mit einem Nachteil behaftet: die fehlende Typisierung. Wie Sie gesehen haben, lässt sich eine ArrayList zwar sehr gut als Datenspeicher verwenden, aber es kann nicht sichergestellt werden, dass von der ArrayList ausschließlich ein bestimmter Datentyp gespeichert wird. Um das Problem zu lösen, mussten typisierte Collections bisher explizit implementiert werden. Dabei wurden Entwickler von den beiden Klassen CollectionBase und DictionaryBase unterstützt. Wie Sie weiter oben gesehen haben, ist die Codierung einer typisierten Klasse auf Grundlage einer dieser beiden Basisklassen allerdings nicht trivial. Mit Generics ändert sich das, denn die Typisierung wird wesentlich vereinfacht.


Galileo Computing

7.4.1 Die Typproblematik am Beispiel der Klasse »Stack«  downtop

Der folgende Code zeigt die benutzerdefinierte Klasse Stack mit den beiden Methoden Push und Pop. Die internen Daten werden in einem object-Array gespeichert. Push erwartet in seinem Parameter ebenfalls object, und Pop liefert object an den Aufrufer zurück.


class Stack {
  private readonly int size;
  private object[] elements;
  private int pointer = 0;
  public Stack(int size) {
    this.size = size;
    elements = new object[size];
  }
  public void Push(object element) {
    if (pointer >= this.size)
      throw new StackOverflowException();
    elements[pointer] = element;
    pointer++;
  }
  public object Pop() {
    pointer--;
    if (pointer >= 0)
      return elements[pointer];
    else {
      pointer = 0;
      throw new InvalidOperationException("Der Stack ist leer");
    }
  }
  public int Length { 
    get { return this.pointer; }
  }
}

Instanziiert werden kann diese Klasse nur über den Aufruf des einfach parametrisierten Konstruktors, dem ein int übergeben wird, mit dem die Größe des Stacks initialisiert wird. Mit Push wird der Stack-Instanz ein Objekt übergeben. Sollte zu diesem Zeitpunkt die Kapazität des Stacks bereits ausgeschöpft sein, ist eine Ausnahme die Folge. Genauso reagiert auch die Methode Pop, falls sich kein Element mehr auf dem Stack befindet.

Grundsätzlich können sowohl Referenz- als auch Wertetypen auf den Stack geschoben werden. Handelt es sich um Wertetypen, müssen die Daten während des Push-Vorgangs geboxt werden. Beim Auslesen aus dem Stack kommt es zum Unboxing, was sich in einer schlechten Performance bemerkbar macht. Neben diesem Nachteil lässt sich auch noch ein weiterer erkennen: Die Typüberprüfung kann nicht mehr vom Compiler durchgeführt werden, sondern verlagert sich in die Laufzeit. Das folgende Codefragment soll das verdeutlichen:


Stack stack = new Stack(10);
stack.Push(2);
string str = (string)stack.Pop();

Dieser Code wird bei der Konvertierung zu einem Laufzeitfehler führen, weil das zuletzt auf den Stack gelegte Element vom Typ int ist und zur Laufzeit der Anwendung versucht wird, den Integer in eine Zeichenfolge zu casten.

Wie Sie sehen können, birgt die Flexibilität des Typs object in sich gravierende Nachteile. Sie könnten natürlich versuchen, mehrere Klassen zu entwickeln, die auf einen bestimmten Datentyp spezialisiert sind, z.B.:


public class StackInt {
  private int[] elemente
  public void Push(int number) {...}
  ...
}

Die Typsicherheit wäre damit gewährleistet, daran besteht kein Zweifel. Andererseits führt dieser Ansatz möglicherweise zu einer größeren Anzahl ähnlicher Klassen, die jeweils nur für einen spezifischen Typ geeignet sind. Solche Lösungen sind zwar machbar, allerdings auch schlecht zu warten. Zudem könnte sich in naher oder ferner Zukunft der Bedarf nach einem weiteren Stack mit einem noch nicht berücksichtigten Typ ergeben, so dass man rückblickend mit dem Ergebnis nicht zufrieden wäre. Genau an dieser Stelle spielen Generics ihre ganze Stärke aus.


Galileo Computing

7.4.2 Die Lösung mit einer generischen Klasse  downtop

Generics erlauben die Verwendung von unbekannten Datentypen. Dazu wird anstelle eines konkreten Datentyps eine Art Platzhalter angegeben. Dieser wird von der Klasse typisiert benutzt. Die Platzhalter werden in spitzen Klammern angegeben und innerhalb der Klasse wie reguläre Datentypen verwendet.


// -------------------------------------------------------------
// Beispiel: ...\Kapitel 7\GenericDemo
// -------------------------------------------------------------
class Stack<T> {
  private readonly int size;
  private T[] elements;
  private int pointer = 0;
  public Stack(int size) {
    this.size = size;
    elements = new T[size];
  }
  public void Push(T element) { ... }
  public T Pop() { ... }
}

Nutzen Sie eine generische Klasse wie Stack<T>, teilen Sie dem Compiler mit, durch welchen Datentyp T ersetzt werden soll. T wird auch als generischer Typparameter bezeichnet. In der folgenden Anweisung handelt es sich um int:


Stack<int> genStack = new Stack<int>(10);

Alle Methoden der Klasse, die T als Parameter definieren oder zurückgeben, akzeptieren jetzt nur noch Integerwerte. Den Zugriff auf die generische Klasse Stack<T> zeigt nachfolgend die Methode Main. Der Code ist in einen try-Block gefasst, um ausgelöste Ausnahmen behandeln zu können. In Kapitel 9 werden wir das Thema Ausnahmen (Exceptions) noch ausgiebig behandeln.


static void Main(string[] args) {
  try {
    Stack<int> stack = new Stack<int>(10);
    stack.Push(123);
    stack.Push(4711);
    stack.Push(34);
    for (int i = stack.Length; i >= 1; i--) {
      Console.WriteLine(stack.Pop());
    }
    stack.Pop();
  }
  catch (Exception e) {
    Console.WriteLine(e.Message);
  }
  Console.ReadLine();
}

Mehrere Typparameter

Die zuvor verwendete generische Klasse Stack benötigte einen variablen Datentyp. Je nachdem, wie die zu entwickelnde Klasse aussehen soll, kann der Bedarf an Platzhaltern jedoch variieren. Ein typisches Beispiel dazu wäre eine Dictionary-Klasse, die nicht nur hinsichtlich des Schlüssels, sondern auch im Hinblick der zugeordnete Wert individuell typisiert werden soll. In solchen Fällen erlaubt C# die Angabe mehrerer Platzhalter, die innerhalb der spitzen Klammer mittels Kommata getrennt werden.


public class Dictionary<K, V> {
  public void Add(K key, V value) { ... }
  public V this[K key] { ... }
}

Der Zugriff auf eine Klasse mit mehreren Typparametern erfolgt genauso wie oben am Beispiel der Klasse Stack gezeigt. Jedem typisierten Parameter wird ein bekannter Typ in spitzen Klammern übergeben:


Dictionary<int, MyClass> myDict = new Dictionary<int, MyClass>();
myDict.Add(1, new MyClass());
MyClass cls = myDict[1];

Das Schlüsselwort »default«

Im Beispiel GenericDemo wird eine Exception geworfen, wenn die Methode Pop aufgerufen wird, der Stack aber bereits geleert ist. Eine andere Lösung hätte vermutlich auch zum Ziel geführt: eine Rückgabe mit return.


public T Pop() {
  pointer--;
  if (pointer >= 0)
    return elements[pointer];
  else {
    pointer = 0;
    return null;
  }
}  

Dieser Ansatz ist richtig, solange der parametrisierte Typ T den Referenztypen zugerechnet werden kann. Handelt es sich jedoch um einen Wertetyp, wird die Laufzeit in einem Desaster enden, da null einem Wertetyp nicht zugewiesen werden kann; die Rückgabe müsste dann 0 sein.

Die Lösung des Problems führt über das Schlüsselwort default. Dieses kann zwischen Referenz- und Wertetypen unterscheiden und liefert null, wenn es sich bei dem konkreten Typ um einen Referenztyp handelt, und 0, wenn es ein den Wertetypen zugerechneter Typ ist.


public T Pop() {
  pointer--;
  if (pointer >= 0)
    return elements[pointer];
  else {
    pointer = 0;
    return default(T);
  }
}  


Galileo Computing

7.4.3 Typparameter mit Constraints einschränkedowntop

Mit der Definition


public class GenericClass<T> { ... }

teilen wir dem Compiler mit, dass der spätere Datentyp zum Zeitpunkt der Klassenentwicklung noch völlig unbekannt ist. Es kann jeder x-beliebige Datentyp verwendet werden.

Wollen Sie innerhalb des Codes der generischen Klasse ein bestimmtes Klassenmitglied des verwendeten Typs aufrufen (beispielsweise eine Methode), ist eine explizite und damit auch unsichere Konvertierung notwendig. Eventuelle Fehler, weil der verwendete Datentyp dieses Klassenmitglied nicht veröffentlicht, würden erst zur Laufzeit der Anwendung erkannt.

Um die Problematik zu verstehen, sehen Sie sich das folgende Beispiel an. Die Klasse SortedList<T> hat eine Methode Add, mit der ein Element der Auflistung hinzugefügt wird. Das neue Element soll nach typspezifischen Kriterien eingeordnet werden. Dazu ist ein Vergleich mit den schon enthaltenen Elementen notwendig, zu dem die Methode CompareTo der Schnittstelle IComparable genutzt wird.


public class SortedList<T> {
  T[] arr = new T[100];
  public T[] Add(T element) {
    for(int i = 0; i < arr.Length; i++) {
      int result = ((IComparable)element).CompareTo(arr[i]);
      ...
    }
    ...
  }
}

Implementiert der Datentyp IComparable nicht, wird eine Ausnahme ausgelöst.

Um diesem Problem aus dem Weg zu gehen, lassen sich die Platzhalter mit Constraints versehen. Ähnlich einer SQL-Abfrage werden diese nach dem Schlüsselwort where notiert.


public class SortableList<T> where T : IComparable { ... }

Jetzt legen wir eine Bedingung fest, an die sich der spätere konkrete Typ halten muss: Er muss die aufgeführte Schnittstelle unterstützen.

In Add können wir nun sogar auf die explizite Konvertierung verzichten, denn wir schreiben die Implementierung der Schnittstelle dem Typ T vor:


int result = element.CompareTo(arr[i]);

Eine Bedingung ist nicht auf Schnittstellen beschränkt; Sie können auch eine Klasse angeben und legen damit die Basisklasse des an den Typparameter T übergebenen konkreten Typs fest.

Mehrere Constraints definieren

Typparameter, die keinen Constraint aufweisen, werden als ungebundene Typparameter bezeichnet, mit Constraint als gebundene Typparameter. Im Bedarfsfall dürfen Sie auch mehrere Constraints angeben, die durch Kommata voneinander getrennt werden.


public class SortableList<T> where T : IComparable, ICloneable { ... }

Das Vorgehen mit dem Doppelpunkt erinnert an eine Klassenableitung. T muss in unserer Definition die angeführten Schnittstellen implementieren. Sie können auch die Basisklasse von T angeben und anschließend im Programmcode auf die Typkonvertierung verzichten und direkt auf die Mitglieder des parametrisierten Datentyps zugreifen.

Es lassen sich auch Bedingungen für mehrere Platzhalter festlegen. Dazu müssen Sie den Constraint für jeden einzelnen Platzhalter mit where einleiten:


public class SpezializedList<T, K> 
                    where T : IComparable, ICloneable 
                    where K : SomeBaseClass
{ ... }

Der Konstruktor-Constraint »new()«

Nehmen wir an, Sie möchten in einer generischen Klasse ein Objekt vom Typ eines generischen Typparameters erzeugen. Das Problem dabei ist, dass der C#-Compiler nicht weiß, ob der Typparameter einen passenden Konstruktor hat. Die Folge wäre ein Kompilierfehler.

Um in dieser Situation eine Lösung zu bieten, können Sie der Liste der Constraints new() anhängen, wie im folgenden Codefragment gezeigt wird:


public class MyList<K, V> where V : new() {
  public K key = default(K);
  public V value;
  public MyList() {
    value = new V();
  }
}

In einer Constraint-Liste steht new() grundsätzlich immer am Ende, und Sie treffen damit eine entscheidende Aussage: Der gewählte Argumenttyp muss einen öffentlichen, parameterlosen Konstruktor unterstützen. Einen parametrisierten Konstruktor vorzuschreiben ist nicht möglich.


Galileo Computing

7.4.4 Generische Methodedowntop

Generische Typen sind nicht nur im Zusammenhang mit Klassen möglich, sondern auch mit Methoden. Dabei ist es nicht zwingend notwendig, dass die Typparameter einer Methode denen der Klasse entsprechen:


class GenericClass<T> {
  public void GenericMethod<K>(K obj) { ... }
}

Im Gültigkeitsbereich der Klasse ist in diesem Fall der Typ T bekannt, K nur innerhalb der Methode.

Sie dürfen generische, methodenspezifische Typparameter auch angeben, wenn die Klasse selbst keine definiert:


class MyClass {
  public void GenericMethod<K>(K obj) { ... }
}

Eine Einschränkung sollten Sie sich aber merken:


Eigenschaftsmethoden und Indexer unterstützen nur Typparameter, die sich im Gültigkeitsbereich der Klasse befinden.

Der Aufruf einer Methode mit generischen Typparametern ist sehr einfach. Sie instanziieren in gewohnter Weise zuerst die Klasse und rufen die Methode unter Angabe des gewünschten konkreten Datentyps auf:


GenericClass<string> obj = new GenericClass<string>();
obj.GenericMethod<int>(25);

Sie können sogar auf die Typangabe verzichten, denn auch in diesem Fall wird der C#-Compiler die richtige Schlussfolgerung ziehen:


obj.GenericMethod(25);

Dieser Aufruf ist absolut gleichwertig.

Methoden und Constraints

Muss der generische Typparameter einer Methode bestimmten Bedingungen genügen, legen Sie einen Constraint fest. Die Syntax entspricht der der Constraints einer Klasse. Allerdings ist es nicht möglich, einen Constraint für einen generischen Typparameter einer Methode zu definieren, der bereits auf Klassenebene festgelegt ist.

Gleichnamige generische Typparameter

Dass Felder auf Klassenebene und Methodenparameter gleichnamig sein dürfen, ist Ihnen bekannt. Diese Freizügigkeit haben Sie mit generischen Typparametern nicht: Ein Platzhalter, der auf Klassenebene angegeben ist, darf für eine Methode nicht mehr verwendet werden, da der C#-Compiler nicht in der Lage ist, diese Doppeldeutigkeit aufzulösen.


class GenericClass<T> {
  // Fehlerhafter Typparameter
  public T GenericMethod<T>(T obj) { ... }
}

Statische, generische Methoden

Generische Typparameter und Constraints können sowohl für Instanz- als auch für statische Methoden festgelegt werden, z.B.:


class GenericClass<T> {
  public  static void GenericMethod<K>(T obj1, K obj2) { ... }
}

Der Aufruf erfolgt mit


GenericClass<string>.GenericMethod<int>("Hallo", 44);

oder in verkürzter Form mit


GenericClass<string>.GenericMethod("Hallo", 44);


Galileo Computing

7.4.5 Generics und Vererbung  downtop

Generische Klassen können abgeleitet werden. Die Regeln sind ähnlich denen, die wir schon kennen. Aufgrund der besonderen Natur generischer Klassen sind dabei jedoch ein paar Besonderheiten zu beachten.

Ist die Basisklasse generisch, kann die abgeleitete Klasse den generischen Typparameter übernehmen und selbst generisch sein.


class BaseClass<T> { ... }
class SubClass<T> : BaseClass<T> { ... }

Die Basisklasse könnte die konkreten Datentypen durch einen Constraint auf ganz bestimmte Typen eingrenzen. Diese gilt dann auch für die abgeleitete Klasse und muss hinter der Basisklasse angegeben werden.


class BaseClass<T> where T : IComparable { ... }
class SubClass<T> : BaseClass<T> where T : IComparable { ... }

Soll die abgeleitete Klasse nicht generisch sein, muss der Typparameter in der Angabe der Basisklasse durch einen konkreten Datentyp ersetzt werden, wie nachfolgend gezeigt:


class BaseClass<T> { ... }
class SubClass : BaseClass<int> { ... }

Sie können umgekehrt auch dann eine generische Subklasse entwickeln, wenn die Basisklasse nicht generisch ist.

Sind in der Basisklasse virtuelle Methoden definiert, wird es noch einmal spannend, denn die Methode könnte in der Basisklasse einen generischen Typparameter haben. Virtuelle Methoden können mit override überschrieben werden. Ob der generische Typparameter durch einen konkreten Datentyp ersetzt werden muss oder ob der Typparameter auch in der überschreibenden Methode angeführt werden darf, entscheidet sich schon bei der Festlegung der Subklasse.

Spielen wir den Fall durch, dass die ableitende Klasse den geerbten generischen Typparameter konkret ersetzt, also:


class BaseClass<T> {
  public virtual T MyMethod() { ... }
}
class SubClass : BaseClass<int> {
  public override int MyMethod() { ... }
}

Wie weiter oben beschrieben, muss der Typparameter durch eine konkrete Angabe ersetzt werden. Das verpflichtet auch dazu, den gewünschten Datentyp in der Signatur der überschreibenden Methode zu benennen. Dass sich die Methode polymorph verhalten wird, bedarf kaum noch einer Erwähnung.

Soll auch die abgeleitete Klasse generisch sein, muss die virtuelle Methode mit generischen Typparametern überschrieben werden.


class BaseClass<T> {
  public virtual T MyMethod() { ... }
}
class  SubClass<T> : BaseClass<T> {
  public override T MyMethod() { ... }
}


Galileo Computing

7.4.6 Konvertierung von Generics  downtop

Eine implizite Konvertierung eines generischen Typparameters ist nur statthaft, wenn der Zieldatentyp object ist oder einer der Typen, die als Constraint hinter where angeführt sind.


class ClassB<T> where T : ClassA, IComparable {
  public void MyMethod(T obj) {
    IComparable var1 = obj;
    ClassA var2 = obj;
    object var3 = obj;
  }
}

Die Klasse ClassB beschreibt den Typparameter T, der den folgenden Bedingungen genügen muss: Der konkrete Typ muss von der Klasse ClassA abgeleitet sein und das Interface IComparable implementieren. Die Zuweisungen in MyMethod sind damit gültig und typsicher, es wird kein Kompilierfehler erscheinen.

An ClassB wollen wir nun noch eine Manipulation vornehmen, indem wir auf die Constraints verzichten. Wir haben dann immer noch die Möglichkeit, implizit in object zu casten, die Konvertierung in eine Schnittstelle muss jedoch explizit erfolgen. Weil der Compiler zur Kompilierzeit nicht weiß, durch welchen konkreten Typ der Typparameter zur Laufzeit ersetzt wird, wird er diesen Cast akzeptieren. Nicht erlaubt ist hingegen die explizite Konvertierung in irgendeine Klasse.


class B<T> {
  public void MyMethod(T obj) {
    IComparable var1 = (IComparable)obj;  // korrekt !!!
    ClassA var2 = (ClassA)obj;            // fehlerhaft !!!
    object var3 = obj;
  }
}

Es bleibt festzustellen, dass das explizite Casten nicht ganz ungefährlich ist und zur Laufzeit eine Ausnahme verursachen kann, wenn der generische Typ nicht die Schnittstelle implementiert. Um dieser Gefahrenquelle aus dem Weg zu gehen, bietet sich eine Alternative mit den beiden Operatoren is und as an. Zur Erinnerung: Mit beiden lässt sich der Typ einer Referenz überprüfen. is liefert true zurück, wenn der linke Operand vom Typ des rechten ist. Der as-Operator führt in diesem Fall sogar eine Konvertierung durch, andernfalls ist der Rückgabewert null.

Das folgende Codefragment zeigt, wie Sie die genannten Operatoren zur Typüberprüfung einsetzen können.


class ClassB<T> {
  public void MyMethod(T obj) {
    if (obj is string) {...}
    if (obj is IComparable) {...}
    // alternativ:
    int intVar = obj as string;
    if (intVar != null) {...}
    IComparable temp = obj as IComparable;
    if (temp != null) {...}
  }
}


Galileo Computing

7.4.7 Generische Delegate  downtop

Delegate können außerhalb des Gültigkeitsbereichs einer Klasse oder in einer Klasse selbst definiert werden. Das gilt auch für Delegate, die einen generischen Typparameter beschreiben. Generische Delegate erweisen sich als besonders nützlich, wenn mehrere ähnliche Events ausgelöst werden. Ein kleiner Satz generischer Delegate, die sich in der Anzahl und dem Typ der Parameter unterscheiden, reicht oftmals vollkommen aus, um alle Ereignishandler bedienen zu können.

Sehen wir uns einen generischen Delegaten an, der außerhalb des Gültigkeitsbereichs einer Klasse definiert ist:


public delegate void MyDelegate<T>(T obj);

Auch hier gibt T den Parametertyp vor. Instanziiert wird ein generischer Delegat in der gleichen Weise wie jeder andere, also entweder


MyDelegate<int> del = new MyDelegate<int>(MyEventHandler);

oder einfach nur mit


MyDelegate<int> del = MyEventHandler;

Die Definition eines generischen Delegaten erlaubt uns außerdem, den konkreten Typ mit where zu beschränken. Wollen Sie beispielsweise den Typparameter T des Delegaten MyDelegate auf die Typen begrenzen, die von der Klasse ClassA abgeleitet sind und die Schnittstelle IMyInterface implementieren, würde die Anweisung wie folgt lauten:


public delegate void MyDelegate<T>(T obj) where T : ClassA, IMyInterface;


Galileo Computing

7.4.8 Generische Klassen in der .NET-Klassenbibliothek  downtop

Die Generics haben erst in der Version 2.0 ihren Einzug in das .NET Framework gefunden und die Klassenbibliothek beeinflusst. Beispielsweise enthält .NET 2.0 mit System. Collections.Generic einen neuen Namespace, der eine Reihe generischer Auflistungen beinhaltet. Dieser Namespace enthält unter anderem auch eine Klasse Stack<T>, ähnlich der, die wir zu Anfang unserer Ausführungen zu den Generics beschrieben haben. Altbekannte Klassen des Frameworks 1.0/1.1 sind um generische Methoden ergänzt worden. Hier sei exemplarisch nur die Klasse Array erwähnt.

Viele andere Klassen und Schnittstellen des Namespace System.Collections finden ein generisches Pendant in System.Collections.Generic. In der folgenden Tabelle sind die wichtigsten Klassen und Schnittstellen des neuen Namespace aufgeführt nebst dem nichtgenerischen Implementierungen.


Tabelle 7.6   Generische Klassen und Schnittstellen und ihre Pendants

System.Collections.Generic System.Collections
Collection<T> CollectionBase
Dictionary<K, V> Hashtable
IComparer<T> IComparer
IComparable<T> IComparable
IEnumerable<T> IEnumerable
IList<T> IList
List<T> ArrayList
Queue<T> Queue
SortedDictionary<K, V> SortedList
Stack<T> Stack


Galileo Computing

7.4.9 Eigene Auflistungen mit »yield« durchlaufedowntop

Nehmen wir an, wir hätten eine Klassendefinition wie folgt:


public class Months {
  string[] months = { "Januar", "Februar", "März", "April", 
                      "Mai", "Juni", "Juli", "August", 
                      "September", "Oktober", "November", "Dezember"};
}

Wäre es nicht schön, mit einer foreach-Schleife den Datenspeicher des Objekts months zu durchlaufen und Zugriff auf alle Elemente zu erhalten, etwa wie folgt:


Months monate = new Months();
foreach (string temp in monate) {
  Console.WriteLine(temp);
}

Dass daran Bedingungen geknüpft sind, habe ich in Abschnitt 7.3 eingangs schon erwähnt. Die Klasse Months muss dazu die Schnittstelle IEnumerable implementieren.


public class Months : IEnumerable

Die einzige in IEnumerable definierte Methode GetEnumerator liefert ein Objekt, das wiederum die Schnittstelle IEnumerator unterstützt.


IEnumerator GetEnumerator ();

Das IEnumerator-Objekt muss die Methoden MoveNext und Reset sowie die Eigenschaft Current implementieren. Damit wird das Durchlaufen der Klasse mit foreach möglich.

Die Beschreibung macht deutlich, dass einiges an Tippaufwand für die Codierung erforderlich ist. So war es jedenfalls im .NET Framework 1.0/1.1. Mit der Version 2.0 wird alles viel einfacher. Sie müssen zwar immer noch die Schnittstelle IEnumerable oder deren generisches Pendant und damit auch die Methode GetEnumerator implementieren, benötigen aber keinen IEnumerator-Typ mehr. Stattdessen liefern Sie die Daten nur noch mit dem neuen Schlüsselwort yield gefolgt von return aus.


// --------------------------------------------------------------
// Beispiel: ...\Kapitel 7\YieldDemo
// --------------------------------------------------------------
class Program {
  static void Main(string[] args) {
    Months months = new Months();
    foreach(string temp in months)
      Console.WriteLine(temp);
    Console.ReadLine();
  }
}
public class Months : IEnumerable {
  string[] month = { "Januar", "Februar", "März", "April", 
              "Mai", "Juni", "Juli", "August", "September", 
              "Oktober", "November", "Dezember"};
  // Methode der Schnittstelle 'IEnumerable'
  public IEnumerator GetEnumerator() {
    for (int i = 0; i < month.Length; i++)
      yield return month[i];
  }
}

yield in Kombination mit return wird zur Angabe des zurückgegebenen Wertes verwendet. Bei Erreichen von yield return wird die aktuelle Position gespeichert und beim nächsten Aufruf der Schleife die Ausführung von dieser Position neu gestartet. Mehr haben Sie als Entwickler nicht zu tun, denn im Hintergrund generiert der Compiler automatisch die Methoden Current und MoveNext der IEnumerator-Schnittstelle, wenn er yield erkennt.

Sie können das Programm sogar noch einfacher schreiben und auf die Implementierung von IEnumerable verzichten. Überlassen Sie einfach alles dem Compiler und yield return. Dazu schreiben Sie ebenfalls eine Methode, deren spezielle Aufgabe es ist, die Objektmenge zurückzuliefern. Allerdings dürfen Sie jetzt den Methodenbezeichner frei vergeben. Der Rückgabewert ist ein Objekt, das die Schnittstelle IEnumerable implementiert und somit auch implizit die Methode GetEnumerator. Hinter den Kulissen wird der Compiler dafür sorgen, dass der Iterator der anfragenden foreach-Schleife alle Daten der Reihe nach übergibt.

Das Beispiel YieldDemo_2 zeigt Ihnen, wie einfach jetzt der Code ist. Beachten Sie bitte auch, dass in der foreach-Schleife nun die Methode GetList für die Bereitstellung der Objekte sorgt. In diesem Code wird die generische Schnittstelle IEnumerable<T> angegeben, Sie können natürlich auch die untypisierte benutzen, was in diesem Fall gleichwertig ist.


// -----------------------------------------------------------
// Beispiel: ...\Kapitel 7\YieldDemo_2
// -----------------------------------------------------------
class Program {
  static void Main(string[] args) {
    Months months = new Months();
    foreach(string temp in months.GetList())
      Console.WriteLine(temp);
    Console.ReadLine();
  }
}
public class Months {
  string[] month = { "Januar", "Februar", "März", "April", 
                     "Mai", "Juni", "Juli", "August", "September", 
                     "Oktober", "November", "Dezember"};
  public IEnumerable<string> GetList() {
    for (int i = 0; i < month.Length; i++)
      yield return month[i];
  }
}

Weitere Möglichkeiten

Die Kombination yield return ist für den Compiler der Anstoß, automatisch einen Iterator zu erzeugen, der von einer foreach-Schleife genutzt werden kann.

Sie können auch mehrfach hintereinander yield aufrufen, wie das folgende Codefragment zeigt.


// Methode der Schnittstelle 'IEnumerable'
public IEnumerator GetEnumerator() {
  yield return "Aachen";
  yield return "Düsseldorf";
  yield return "Köln";
}

Zum Abbruch einer Iteration kombinieren Sie yield mit break:

yield break;

Einschränkungen von »yield return«

Der Einsatz von yield return unterliegt zwei Einschränkungen:

gp  yield return kann nicht innerhalb einer anonymen Methode codiert werden.
gp  yield return darf weder in einem catch-Block noch in einem try-Block verwendet werden, wenn letzterer eine catch-Klausel hat. Die Verwendung in einem try-Block, dem sich nur noch ein finally-Block anschließt, ist jedoch möglich.

Auch an dieser Stelle muss ich noch einmal darauf verweisen, dass Sie in Kapitel 9 alles über die Behandlung von Ausnahmen (Exceptions) mit try-catch erfahren werden.


Galileo Computing

7.4.10 Daten durch »null« beschreiben  toptop

Angenommen Sie greifen auf das Feld einer Tabelle in einer Datenbank zu. Der Datentyp des Feldes sei ein Integer. Damit ist der zulässige Wertebereich eines initialisierten Feldes bereits exakt beschrieben, der zwischen dem Minimal- und dem Maximalmalwert des Typs liegt. Spalten einer Datenbanktabelle müssen aber nicht zwangsläufig mit einem durch den Datentyp beschriebenen Wert gefüllt sein, sie dürfen auch leer bleiben und werden trotzdem als gültig anerkannt. In diesem Fall ist neben einem Zahlenwert auch null ein akzeptierter Inhalt.

Probleme dieser Art können nun ganz einfach durch Nullable Typen gelöst werden. Dabei spielt die im .NET Framework eingeführte Klasse System.Nullable<T> die entscheidende Rolle. Die Signatur deutet bereits an, dass die Klasse Generics benutzt. Dabei wird einem Datentyp die Verwendung von null ermöglicht. Das macht natürlich nur Sinn, wenn es sich bei dem Datentyp um einen Typ handelt, der den Wertetypen zugerechnet wird. Referenztypen unterstützen bekanntlich grundsätzlich null.

Grundsätzlich kann die Klasse wie folgt verwendet werden:


Nullable<int> x = 4711;
Nullable<int> y = null;

C# verfügt darüber hinaus auch über eine eigene Sprachsyntax. Dafür wurde der neue Modifizierer »?« eingeführt, der aus einem Datentyp einen null-fähigen Typen macht. Damit kann die Notation der beiden Anweisungen auch vereinfachend wie folgt lauten:


int? x = 4711;
int? y = null;

Da wir es jetzt mit einem neuen Datentypen zu tun haben, der auch null unterstützt, wird in der Klasse Nullable mit HasValues eine Eigenschaft angeboten, die einen booleschen Wert beschreibt. Er ist true, wenn der Inhalt der null-fähigen Variable einen gültigen Wert aufweist, also ungleich null ist.


if (x.HasValue)
  Console.WriteLine("Wert ist ungleich null");
else
  Console.WriteLine("Wert = null");

Die Ausgabe würde hier demnach lauten Wert ist ungleich null.

Der Inhalt der Variablen kann mit der Eigenschaft Value abgefragt werden. Sie liefert einen gültigen Wert, wenn HasValue true ist. Ansonsten wird eine Ausnahme vom Typ InvalidOperationException ausgelöst.

Darüber hinaus können Sie Nullable Typen auch in der üblichen Form eines Referenztypen verwenden und beispielsweise mit null vergleichen:


if(x != null) {
  ...
}

Konvertierungen mit Nullable Typen

Ein Nullable Typ ist gegenüber seinem zugrunde liegenden Datentyp um die Fähigkeit, auch null zu unterstützen, erweitert worden. Eine Zuweisung wie im folgenden Codefragment kommt einer aufweitenden Operation gleich und wird daher implizit vorgenommen.


int x = 20;
int? y = x;

Solch im umgehrten Fall die Zuweisung eines null-fähigen Typ an seinen elementaren Typ erfolgen, muss explizit konvertiert werden.


int? x = 20;
int y = (int)x;

Hat x in diesem Beispiel den Inhalt null, wird eine Ausnahme ausgelöst.

 << zurück
  
  Zum Katalog
Zum Katalog: Visual C# 2005
Visual C# 2005
bestellen
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Fortgeschrittene Programmierung mit Visual C# 2005






 Fortgeschrittene
 Programmierung
 mit Visual C# 2005


Zum Katalog: Einstieg in Visual C# 2005






 Einstieg in
 Visual C# 2005


Zum Katalog: Einstieg in Visual Basic 2005






 Einstieg in
 Visual Basic 2005


Zum Katalog: Visual Basic 2005






 Visual Basic 2005


Zum Katalog: Java ist auch eine Insel






 Java ist auch eine
 Insel


Zum Katalog: Konzepte und Lösungen für Microsoft-Netzwerke






 Konzepte und
 Lösungen für
 Microsoft-Netzwerke


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo








Copyright © Galileo Press 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de